package org.ydeity.lucene.core;

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.IntPoint;
import org.apache.lucene.document.LongPoint;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.ydeity.lucene.configuration.LuceneDocumentProperties;
import org.ydeity.lucene.configuration.LuceneFieldProperties;
import org.ydeity.lucene.convert.MappingLuceneConvert;
import org.ydeity.lucene.exception.LuceneException;

import java.io.IOException;
import java.io.Serializable;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

public class LuceneTemplate implements LuceneOperations {

    private MappingLuceneConvert mappingLuceneConvert;

    public LuceneTemplate(MappingLuceneConvert mappingLuceneConvert) {
        this.mappingLuceneConvert = mappingLuceneConvert;
    }

    @Override
    public <T> boolean doInsert(T t) {
        this.checkDataExist(t);
        LuceneDocumentProperties documentProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(t.getClass());

        IndexWriter indexWriter = documentProperties.getIndexWriter();
        try {
            indexWriter.addDocument(this.mappingLuceneConvert.getLuceneDocument(t));
            indexWriter.commit();
            indexWriter.close();

            return true;
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
    }

    private <T> void checkDataExist(T t){
        Serializable id = this.mappingLuceneConvert.getId(t);
        Optional<?> optional = Optional.empty();
        try {
            optional = this.getById(t.getClass(), id);
        } catch (LuceneException e) {
            // nothing to do
        }
        if(optional.isPresent()){
            throw new LuceneException("data exist:" + id + " - " + t.toString());
        }
    }

    @Override
    public <T> boolean doUpdate(T t) {
        String idProperty = this.mappingLuceneConvert.getIdProperty(t.getClass());
        Serializable id = this.mappingLuceneConvert.getId(t);

        Optional<?> optional = this.getById(t.getClass(), id);
        if(!optional.isPresent()){
            throw new LuceneException("document not found:" + id);
        }
        Document doc = this.mappingLuceneConvert.getExistDocument(optional.get(),t);

        LuceneDocumentProperties documentProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(t.getClass());
        IndexWriter indexWriter = documentProperties.getIndexWriter();

        try {
            long l = indexWriter.updateDocument(new Term(idProperty, String.valueOf(id)), doc);

            indexWriter.commit();
            indexWriter.close();
            return l > 0;
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
    }

    @Override
    public <T> int saveAll(Iterable<? extends T> iterable,Class<? extends T> clazz) {
        LuceneDocumentProperties documentProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(clazz);
        IndexWriter indexWriter = documentProperties.getIndexWriter();

        Iterator<? extends T> iterator = iterable.iterator();
        int num = 0;
        while (iterator.hasNext()) {
            T t = iterator.next();

            this.checkDataExist(t);
            try {
                indexWriter.addDocument(this.mappingLuceneConvert.getLuceneDocument(t));
                num ++;
            } catch (IOException e) {
                throw new LuceneException(e.getMessage());
            }
        }

        try {
            indexWriter.commit();
            indexWriter.close();
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
        return num;
    }

    @Override
    public <T> boolean doDelete(Class<? extends T> clazz, Serializable id) {
        LuceneDocumentProperties documentProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(clazz);
        String idProperty = this.mappingLuceneConvert.getIdProperty(clazz);

        IndexWriter writer = documentProperties.getIndexWriter();

        Query query = new TermQuery(new Term(idProperty,String.valueOf(id)));
        try {
            long l = writer.deleteDocuments(query);
            writer.commit();
            writer.close();

            return l > 0;
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
    }

    @Override
    public <T> boolean delete(T t) {
        Serializable id = this.mappingLuceneConvert.getId(t);
        return this.doDelete(t.getClass(),id);
    }

    @Override
    public <T> long deleteAll(Class<T> clazz) {
        LuceneDocumentProperties documentProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(clazz);
        IndexWriter writer = documentProperties.getIndexWriter();

        try {
            long l = writer.deleteAll();

            writer.commit();
            writer.close();
            return l;
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
    }

    @Override
    public <T> long deleteAll(Iterable<? extends T> iterable, Class<T> clazz) {
        int num = 0;
        Iterator<? extends T> iterator = iterable.iterator();
        while(iterator.hasNext()) {
            T t = iterator.next();
            boolean delete = this.delete(t);
            if(delete) {
                num ++;
            }
        }
        return num;
    }

    @Override
    public <T> long count(Class<? extends T> clazz,String field) {
        LuceneDocumentProperties documentProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(clazz);
        IndexSearcher searcher = documentProperties.getIndexSearcher();

        try {
            CollectionStatistics statistics = searcher.collectionStatistics(field);
            return statistics.maxDoc();
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
    }

    @Override
    public <T> Optional<T> getById(Class<? extends T> clazz, Serializable id) {
        String idProperty = this.mappingLuceneConvert.getIdProperty(clazz);

        LuceneFieldProperties fieldProperties = this.mappingLuceneConvert.getLuceneDocumentProperties(clazz).getFieldMapping().get(idProperty);

        List<T> lists = this.queryForObject(clazz,
                this.getPrimaryKeyQuery(fieldProperties.getParamType(),idProperty,id), 1);

        if(!lists.isEmpty()){
            return Optional.of(lists.get(0));
        }
        return Optional.empty();
    }

    private Query getPrimaryKeyQuery(Class<?> paramType,String property,Serializable id){
        Query query = null;
        if(paramType.equals(String.class)){
            QueryParser queryParser = new QueryParser(property, new StandardAnalyzer());
            query = this.getQuery(queryParser,String.valueOf(id));
        }
        if(paramType.equals(Long.class) || paramType.equals(long.class)){
            query = LongPoint.newSetQuery(property, (long) id);
        }
        if(paramType.equals(Integer.class) || paramType.equals(int.class)){
            query = IntPoint.newSetQuery(property, (int) id);
        }
        return query;
    }

    @Override
    public <T> List<T> queryForObject(Class<? extends T> clazz, Query query, int n) {
        return this.query(clazz,query,null,n);
    }

    @Override
    public <T> List<T> queryForObject(Class<? extends T> clazz, Query query, Sort sort, int n) {
        return this.query(clazz,query,sort,n);
    }

    private <T> List<T> query(Class<? extends T> clazz, Query query, Sort sort, int n) {
        LuceneDocumentProperties documentProperties = mappingLuceneConvert.getLuceneDocumentProperties(clazz);
        TopDocs topDocs = null;
        try {
            if(sort != null){
                topDocs = documentProperties.getIndexSearcher().search(query, n,sort);
            } else {
                topDocs = documentProperties.getIndexSearcher().search(query, n);
            }
        } catch (IOException e) {
            return Collections.emptyList();
        }

        return mappingLuceneConvert.convertTopDoc(clazz,topDocs);
    }

    @Override
    public <T> Page<T> search(Class<? extends T> clazz, Query query, Pageable pageable) {
        return this.searchDoc(clazz,query,null,pageable);
    }

    @Override
    public <T> Page<T> search(Class<? extends T> clazz, Query query, Sort sort, Pageable pageable) {
        return this.searchDoc(clazz,query,sort,pageable);
    }

    private <T> Page<T> searchDoc(Class<? extends T> clazz, Query query, Sort sort, Pageable pageable) {
        LuceneDocumentProperties documentProperties = mappingLuceneConvert.getLuceneDocumentProperties(clazz);
        IndexSearcher indexSearcher = documentProperties.getIndexSearcher();
        TopDocs topDocs = null;

        ScoreDoc lastScoreDoc = this.getLastScoreDoc(pageable.getPageNumber(), pageable.getPageSize(), query, indexSearcher);
        try {
            if(sort != null){
                topDocs = indexSearcher.searchAfter(lastScoreDoc,query, pageable.getPageSize(),sort);
            } else {
                topDocs = indexSearcher.searchAfter(lastScoreDoc,query, pageable.getPageSize());
            }
        } catch (IOException e) {
            return new PageImpl<>(Collections.emptyList());
        }

        return new PageImpl<>(mappingLuceneConvert.convertTopDoc(clazz,topDocs),pageable,topDocs.totalHits);
    }

    private ScoreDoc getLastScoreDoc(int pageIndex,int pageSize,Query query,IndexSearcher indexSearcher){
        if(pageIndex==1) return null;
        int num = pageSize * (pageIndex-1);
        TopDocs tds = null;
        try {
            tds = indexSearcher.search(query, num);
        } catch (IOException e) {
            throw new LuceneException(e.getMessage());
        }
        return tds.scoreDocs[num-1];
    }

    private Query getQuery(QueryParser queryParser,String text){
        try {
            return queryParser.parse(text);
        } catch (ParseException e) {
            throw new LuceneException(e.getMessage());
        }
    }
}
